/****************************************************************************
 * Cynk
 * Copyright (C) 2011  Nan Dun <dunnan@yl.is.s.u-tokyo.ac.jp>
 * Department of Computer Science, The University of Tokyo
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * This program can be distributed under the terms of the GNU GPL.
 * See the file COPYING.
 ***************************************************************************/

/*
 * cynk.c
 * Sync utility for Cloud storage
 */

#define _GNU_SOURCE /* avoid implicit declaration of *pt* functions */
#include "config.h"

#ifdef linux
#define _XOPEN_SOURCE 500 /* for pread()/pwrite() */
#endif

#include <fuse.h>
#include <fuse_opt.h>
#include <fuse_lowlevel.h>
#include <ulockmgr.h>
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <dirent.h>
#include <string.h>
#include <stdint.h>
#include <errno.h>
#include <semaphore.h>
#include <pthread.h>
#include <netdb.h>
#include <signal.h>
#include <pwd.h>
#include <sys/uio.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/utsname.h>
#include <sys/mman.h>
#include <sys/poll.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#ifdef HAVE_SETXATTR
#include <sys/xattr.h>
#endif

#include <glib.h>

#if GLIB_CHECK_VERSION(2, 16, 0)
#define GLIB_HASH_TABLE_HAS_ITER
#endif

#define MAX_BUF_LEN		1024
#define TEMP_STR_LEN	7
#define REPO_HOME		".cynk"
#define TRANS_HOME		".cynk/transfer"
#define SESSION_HOME	"/tmp"

struct cynk {
	char *local;
	char *remote;
	char *mountpoint;
	char *session;
	int mode;

	GHashTable *born;
	GHashTable *dead;
	pthread_mutex_t lock_born;
	pthread_mutex_t lock_dead;
	char *repo_root;
	char *trans_root;
	char *exclude_file;
	char *include_file;
	char *rsync_common_opts;

	pthread_mutex_t sync_lock;
	
	int duration;
	int stable;
	struct timespec sysc_stamp;
	pthread_t sync_thread;
	pthread_attr_t sync_thread_attr;
	pthread_mutex_t sync_stop_lock;
	pthread_cond_t sync_stop_cond;
	int sync_started;
	int sync_stop;
	
	int cmd;
	char *cmd_sockpath;
	int cmd_sock;
	pthread_t cmd_thread;
	pthread_attr_t cmd_thread_attr;
	int cmd_started;

	char *progname;
	char *username;
	char *userhome;
	pid_t pid;
	uid_t uid;
	gid_t gid;
	
	int debug;
	int foreground;
	int dryrun;
	int verbosity;
	int verbose_cnt;
};

static struct cynk cynk;

enum {
	KEY_HELP,
	KEY_VERSION,
	KEY_FOREGROUND,
	KEY_DEBUG,
	KEY_DEBUG_ALL,
	KEY_DRYRUN,
	KEY_CMD_SYNC,
	KEY_MODE_MANUAL,
	KEY_DURATION,
	KEY_STABLE,
};

#define CYNK_OPT(t, p, v) { t, offsetof(struct cynk, p), v }

static struct fuse_opt cynk_opts[] = {
	CYNK_OPT("verbosity=%d",		verbosity, 0),
	
	/* Append a space if the option takes an argument */
	FUSE_OPT_KEY("-V",              KEY_VERSION),
	FUSE_OPT_KEY("--version",       KEY_VERSION),
	FUSE_OPT_KEY("-h",              KEY_HELP),
	FUSE_OPT_KEY("--help",          KEY_HELP),
	FUSE_OPT_KEY("-f",              KEY_FOREGROUND),
	FUSE_OPT_KEY("-d", 				KEY_DEBUG),
	FUSE_OPT_KEY("-D",				KEY_DEBUG_ALL),
	FUSE_OPT_KEY("-n",				KEY_DRYRUN),
	FUSE_OPT_KEY("--dry-run", 		KEY_DRYRUN),
	FUSE_OPT_KEY("-S ",				KEY_CMD_SYNC),
	FUSE_OPT_KEY("-m",				KEY_MODE_MANUAL),
	FUSE_OPT_KEY("-t ",				KEY_DURATION),
	FUSE_OPT_KEY("-b ",				KEY_STABLE),
	FUSE_OPT_END
};

static void error2(int errnum, const char *format, ...)
{
	va_list argv;
	fflush(stdout);
	fprintf(stderr, "error: ");
	va_start(argv, format);
	vfprintf(stderr, format, argv);
	va_end(argv);
	fprintf(stderr, ": %s\n", strerror(errnum));
}

static void perror2(const char *format, ...)
{
	va_list argv;
	fflush(stdout);
	fprintf(stderr, "error: ");
	va_start(argv, format);
	vfprintf(stderr, format, argv);
	va_end(argv);
	fprintf(stderr, ": %s\n", strerror(errno));
}

#define error3(format, ...) \
	fprintf(stderr, "error: "format"\n", ## __VA_ARGS__)
#define fatal(status, format, ...) \
	{perror2(format, ## __VA_ARGS__); exit(status);}
#define add_path(path) \
	g_strdup_printf("%s/%s", cynk.local, path[1] ? path + 1 : "")

static char * string_hash(const char *str)
{
	unsigned long hash = 5381;
	int c, len, v, i, j;
	char *s, *res, two[3];
	char *alph = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
	int alph_len = strlen(alph);

	/* djb2 hash function */
	memset(two, 0, sizeof(two));
	while ((c = *str++))
		hash = ((hash << 5) + hash) + c;
	s = g_strdup_printf("%lu", hash);
	len = strlen(s);
	
	j = 0;
	for (i = 0; i < (len / 2 + len % 2); i++) {
		memcpy(two, s + i * 2, 2);
		v = atoi(two);
		if (v < alph_len) {
			s[j] = alph[atoi(two) - 1];
		} else {
			memcpy(s + j, two, 2);
			j++;
		}
		j++;
	}
	s[j] = '\0';
	res = g_strdup_printf("%s", s);
	g_free(s);
	
	return res;
}

static char * rand_str(size_t len)
{
	char *s = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
	int l = strlen(s);
	char *str = g_malloc0(len + 1);
	size_t i;
	
	srand(time(NULL));
	for (i = 0; i < len; i++)
		str[i] = s[rand() % l];

	return str;
}

static double tv_get_elapsed(struct timeval *end, struct timeval *start)
{
	struct timeval res;

	if (end->tv_usec < start->tv_usec) {
		int nsec = (start->tv_usec - end->tv_usec) / 1000000 + 1;
		start->tv_usec -= 1000000 * nsec;
		start->tv_sec += nsec;
	}
	if (end->tv_usec - start->tv_usec > 1000000) {
		int nsec = (end->tv_usec - start->tv_usec) / 1000000;
		start->tv_usec += 1000000 * nsec;
		start->tv_sec -= nsec;
	}
	res.tv_sec = end->tv_sec - start->tv_sec;
	res.tv_usec = end->tv_usec - start->tv_usec;

	return (double) res.tv_sec + ((double) res.tv_usec / 1000000);
}

static double ts_get_elapsed(struct timespec *end, struct timespec *start)
{
	struct timespec res;

	if (end->tv_nsec < start->tv_nsec) {
		int nsec = (start->tv_nsec - end->tv_nsec) / 1000000000 + 1;
		start->tv_nsec -= 1000000000 * nsec;
		start->tv_sec += nsec;
	}
	if (end->tv_nsec - start->tv_nsec > 1000000000) {
		int nsec = (end->tv_nsec - start->tv_nsec) / 1000000000;
		start->tv_nsec += 1000000000 * nsec;
		start->tv_sec -= nsec;
	}
	res.tv_sec = end->tv_sec - start->tv_sec;
	res.tv_nsec = end->tv_nsec - start->tv_nsec;

	return (double) res.tv_sec + ((double) res.tv_nsec / 1000000000);
}

static char * time_str(time_t *now)
{
	struct tm *local;
	char *tm_str;
		
	local = localtime(now);
	tm_str = asctime(local);
	tm_str[strlen(tm_str) - 1] = '\0';

	return tm_str;
}

static void verbose(int index, const char *template, ...)
{
	va_list ap;
	
	if (index) {
		fprintf(stdout, "[%05d] ", cynk.verbose_cnt);
		cynk.verbose_cnt++;
	} else
		fprintf(stdout, "  ");
	va_start(ap, template);
	vfprintf(stdout, template, ap);
	va_end(ap);
	fprintf(stdout, "\n");
	fflush(stdout);
}

static int run(const char *format, ...)
{
	va_list argv;
	FILE *fp;
	char *command;
	char line[2048];
	int res = 0;

	va_start(argv, format);
	command = g_strdup_vprintf(format, argv);
	va_end(argv);

	if (cynk.verbosity >= 3)
		verbose(FALSE, "> %s", command);
	if (!cynk.dryrun) {
		if (!(fp = popen(command, "r")))
			fatal(1, "failed to run command %s", command);
		if (cynk.verbosity >= 3) {
			while (fgets(line, sizeof(line), fp))
				fprintf(stdout, "    %s", line);
		}
		res = pclose(fp);
	}
	return res;
}

static char * rsync_common_opts(void)
{
	char opts[16];
	int index = 0;
	
	memset(opts, 0, sizeof(opts));
	opts[index++] = '-';
	opts[index++] = 'a';
	if (cynk.verbosity >= 3)
		opts[index++] = 'i';
	if (cynk.dryrun)
		opts[index++] = 'n';

	return g_strdup(opts);
}

static void on_born(const char *path, uint32_t dir)
{
	if (g_hash_table_lookup(cynk.dead, path)) {
		if (cynk.verbosity >= 3)
			verbose(FALSE, "dead: remove %s", path);
		pthread_mutex_lock(&cynk.lock_dead);
		g_hash_table_remove(cynk.dead, path);
		pthread_mutex_unlock(&cynk.lock_dead);
	} else {
		if (cynk.verbosity >= 3)
			verbose(FALSE, "born: insert %s", path);
		char *key = g_strdup(path);
		pthread_mutex_lock(&cynk.lock_born);
		g_hash_table_insert(cynk.born, key, GUINT_TO_POINTER(dir));
		pthread_mutex_unlock(&cynk.lock_born);
	}
}

static void on_dead(const char *path, uint32_t dir)
{
	if (g_hash_table_lookup(cynk.born, path)) {
		if (cynk.verbosity >= 3)
			verbose(FALSE, "born: remove %s", path);
		pthread_mutex_lock(&cynk.lock_born);
		g_hash_table_remove(cynk.born, path);
		pthread_mutex_unlock(&cynk.lock_born);
	} else {
		if (cynk.verbosity >= 3)
			verbose(FALSE, "dead: insert %s", path);
		char *key = g_strdup(path);
		pthread_mutex_lock(&cynk.lock_dead);
		g_hash_table_insert(cynk.dead, key, GUINT_TO_POINTER(dir));
		pthread_mutex_unlock(&cynk.lock_dead);
	}
}
#ifndef GLIB_HASH_TABLE_HAS_ITER
struct write_line_data {
	FILE *fp;
};

static void write_line(gpointer key, gpointer value, gpointer data)
{
	struct write_line_data *p = (struct write_line_data *) data;
	fprintf(p->fp, "- %s\n", (char *) key);
}
#endif

static int make_pull_exclude(const char *path)
{
	FILE *fp;

	fp = fopen(path, "w+");
	if (fp == NULL) {
		perror2("failed to open file \"%s\"", path);
		return -1;
	}

	if (cynk.verbosity >= 3)
		verbose(FALSE, "writing exclude rules to %s", path);

#ifdef GLIB_HASH_TABLE_HAS_ITER
	GHashTableIter iter;
	gpointer key, value;

	fprintf(fp, "# Born files\n");
	g_hash_table_iter_init(&iter, cynk.born);
	while (g_hash_table_iter_next(&iter, &key, &value))
		fprintf(fp, "- %s\n", (char *) key);
	
	fprintf(fp, "# Dead files\n");
	g_hash_table_iter_init(&iter, cynk.dead);
	while (g_hash_table_iter_next(&iter, &key, &value))
		fprintf(fp, "- %s\n", (char *) key);
#else
	struct write_line_data data;
	data.fp = fp;
	
	fprintf(fp, "# Born files\n");
	g_hash_table_foreach(cynk.born, write_line, &data);
	
	fprintf(fp, "# Dead files\n");
	g_hash_table_foreach(cynk.dead, write_line, &data);
#endif

	fclose(fp);
	return 0;
}

static int make_push_include(const char *path)
{
	FILE *fp;
	
	fp = fopen(path, "w+");
	if (fp == NULL) {
		perror2("failed to open file \"%s\"", path);
		return -1;
	}

	if (cynk.verbosity >= 3)
		verbose(FALSE, "writing include rules to %s", path);

#ifdef GLIB_HASH_TABLE_HAS_ITER
	GHashTableIter iter;
	gpointer key, value;
	
	fprintf(fp, "# Dead files\n");
	g_hash_table_iter_init(&iter, cynk.dead);
	while (g_hash_table_iter_next(&iter, &key, &value))
		fprintf(fp, "+ %s\n", (char *) key);
#else
	struct write_line_data data;
	data.fp = fp;
	
	fprintf(fp, "# Dead files\n");
	g_hash_table_foreach(cynk.dead, write_line, &data);
#endif

	fclose(fp);
	return 0;
}

static double synchronize(void)
{	
	int res;
	time_t start, end;
	struct timeval tv_start, tv_end;
	double elapsed;
	
	pthread_mutex_lock(&cynk.sync_lock);
	start = time(NULL);
	if (cynk.verbosity >= 1)
		verbose(FALSE, "---------------- SYNC START at %s ----------------", 
			time_str(&start));
	
	if (cynk.verbosity >= 2)
		verbose(FALSE, "PULL phase starting...");
	gettimeofday(&tv_start, NULL);
	make_pull_exclude(cynk.exclude_file);
	res = run("rsync %s/ %s/ --exclude=/%s --temp-dir=%s "
		"--delete --exclude-from=%s %s",
		cynk.remote, cynk.local, REPO_HOME, TRANS_HOME,
		cynk.exclude_file, cynk.rsync_common_opts);
	
	/* prevent very recently created files from being deleted on remote */
	if (cynk.verbosity >= 2)
		verbose(FALSE, "PUSH phase starting...");
	make_push_include(cynk.include_file);
	res = run("rsync %s/ %s/ --exclude=/%s --temp-dir=%s "
		"--delete --include-from=%s --filter='P /*' %s",
		cynk.local, cynk.remote, REPO_HOME, TRANS_HOME,
		cynk.include_file, cynk.rsync_common_opts);
	gettimeofday(&tv_end, NULL);
	
	pthread_mutex_lock(&cynk.lock_born);
	g_hash_table_remove_all(cynk.born);
	pthread_mutex_unlock(&cynk.lock_born);
	
	pthread_mutex_lock(&cynk.lock_dead);
	g_hash_table_remove_all(cynk.dead);
	pthread_mutex_unlock(&cynk.lock_dead);

	elapsed = tv_get_elapsed(&tv_end, &tv_start);
	
	end = time(NULL);
	if (cynk.verbosity >= 1)
		verbose(FALSE, 
			"------- SYNC END at %s, took %.3f seconds -------", 
			time_str(&end), elapsed);
	pthread_mutex_unlock(&cynk.sync_lock);

	return elapsed;
}


static void *process_sync(void *data)
{
	(void) data;
	struct timespec now, wait;
	double timeout;
	
	while (1) {
		pthread_mutex_lock(&cynk.sync_stop_lock);
		clock_gettime(CLOCK_REALTIME, &now);
		wait = now;
		wait.tv_sec += cynk.duration;
		pthread_cond_timedwait(&cynk.sync_stop_cond, &cynk.sync_stop_lock, 
			&wait);
		if (cynk.sync_stop)
			break;
		timeout = ts_get_elapsed(&now, &cynk.sysc_stamp);
		if (timeout > cynk.stable) {
			if (cynk.verbosity >= 3)
				verbose(TRUE, "SYNC: no system call since %.3f secs ago",
					timeout); 
			synchronize();
		} else {
			if (cynk.verbosity >= 3)
				verbose(TRUE, 
					"REST: system call detected %.3f seconds ago",
					timeout); 
		}
		pthread_mutex_unlock(&cynk.sync_stop_lock);
	}

	pthread_exit(NULL);
}

static int start_sync_thread(void)
{
	int err;
	
	if (cynk.verbosity >= 3)
		verbose(TRUE, "START SYNC THREAD: duration=%d, stable=%d", 
			cynk.duration, cynk.stable);

	pthread_mutex_init(&cynk.sync_lock, NULL);
	pthread_attr_init(&cynk.sync_thread_attr);
	pthread_mutex_init(&cynk.sync_stop_lock, NULL);
	pthread_cond_init(&cynk.sync_stop_cond, NULL);

	pthread_attr_setdetachstate(&cynk.sync_thread_attr,
		PTHREAD_CREATE_JOINABLE);
	err = pthread_create(&cynk.sync_thread, NULL, process_sync, NULL);
	if (err) {
		error2(err, "create thread");
		return -EIO;
	}
	pthread_detach(cynk.sync_thread);
	cynk.sync_started = 1;
	
	return 0;
}

static void stop_sync_thread(void)
{
	if (cynk.verbosity >= 2)
		verbose(FALSE, "* Stop sync thread...");
	
	pthread_mutex_lock(&cynk.sync_stop_lock);
	cynk.sync_stop = 1;
	pthread_cond_signal(&cynk.sync_stop_cond);
	pthread_mutex_unlock(&cynk.sync_stop_lock);
	pthread_join(cynk.sync_thread, NULL);
	
	pthread_attr_destroy(&cynk.sync_thread_attr);
	pthread_mutex_destroy(&cynk.sync_stop_lock);
	pthread_cond_destroy(&cynk.sync_stop_cond);

	cynk.sync_started = 0;
}

enum {
	ENTRY_IS_FILE = 1,
	ENTRY_IS_DIR = 2,
	ENTRY_IS_SYMLINK = 3,
	ENTRY_IS_LINK = 4,
};

static int cynk_getattr(const char *path, struct stat *stbuf)
{
	int res;
	char *realpath = add_path(path);

	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);
	
	res = lstat(realpath, stbuf);
	
	g_free(realpath);

	if (res == -1)
		return -errno;
	
	return 0;
}

static int cynk_fgetattr(const char *path, struct stat *stbuf,
			struct fuse_file_info *fi)
{
	int res;

	(void) path;
	
	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);

	res = fstat(fi->fh, stbuf);
	if (res == -1)
		return -errno;

	return 0;
}

static int cynk_access(const char *path, int mask)
{
	int res;
	char *realpath = add_path(path);
	
	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);

	res = access(realpath, mask);

	g_free(realpath);
	if (res == -1)
		return -errno;

	return 0;
}

static int cynk_readlink(const char *path, char *buf, size_t size)
{
	int res;
	char *realpath = add_path(path);
	
	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);

	res = readlink(realpath, buf, size - 1);

	g_free(realpath);
	if (res == -1)
		return -errno;

	buf[res] = '\0';
	return 0;
}

struct cynk_dirp {
	DIR *dp;
	struct dirent *entry;
	off_t offset;
};

static int cynk_opendir(const char *path, struct fuse_file_info *fi)
{
	int res;
	struct cynk_dirp *d = malloc(sizeof(struct cynk_dirp));
	if (d == NULL)
		return -ENOMEM;

	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);
	
	char *realpath = add_path(path);

	d->dp = opendir(realpath);
	if (d->dp == NULL) {
		res = -errno;
		free(d);
		return res;
	}
	d->offset = 0;
	d->entry = NULL;

	fi->fh = (unsigned long) d;
	return 0;
}

static inline struct cynk_dirp *get_dirp(struct fuse_file_info *fi)
{
	return (struct cynk_dirp *) (uintptr_t) fi->fh;
}

static int cynk_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
		       off_t offset, struct fuse_file_info *fi)
{
	struct cynk_dirp *d = get_dirp(fi);
	(void) path;
	
	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);
	
	if (offset != d->offset) {
		seekdir(d->dp, offset);
		d->entry = NULL;
		d->offset = offset;
	}
	while (1) {
		struct stat st;
		off_t nextoff;

		if (!d->entry) {
			d->entry = readdir(d->dp);
			if (!d->entry)
				break;
		}

		memset(&st, 0, sizeof(st));
		st.st_ino = d->entry->d_ino;
		st.st_ino = 100;
		st.st_mode = d->entry->d_type << 12;
		nextoff = telldir(d->dp);
		if (filler(buf, d->entry->d_name, &st, nextoff))
			break;

		d->entry = NULL;
		d->offset = nextoff;
	}

	return 0;
}

static int cynk_releasedir(const char *path, struct fuse_file_info *fi)
{
	struct cynk_dirp *d = get_dirp(fi);
	(void) path;
	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);
	closedir(d->dp);
	free(d);
	return 0;
}

static int cynk_mknod(const char *path, mode_t mode, dev_t rdev)
{
	int res = 0;
	char *realpath = add_path(path);
	
	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);

	if (S_ISFIFO(mode))
		res = mkfifo(realpath, mode);
	else
		res = mknod(realpath, mode, rdev);

	if (res == -1) {
		res = -errno;
	} else {
		if (cynk.verbosity >= 3)
			verbose(TRUE, "MKNOD: %s", realpath);
		on_born(path, ENTRY_IS_FILE); /* use path */
	}
	g_free(realpath);
	return res;
}

static int cynk_mkdir(const char *path, mode_t mode)
{
	int res = 0;
	char *realpath = add_path(path);
	
	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);

	if (mkdir(realpath, mode) == -1) {
		res = -errno;
	} else {
		if (cynk.verbosity >= 3)
			verbose(TRUE, "MKDIR: %s", realpath);
		on_born(path, ENTRY_IS_DIR); /* use path here */
	}	
	g_free(realpath);
	return res;
}

static int cynk_unlink(const char *path)
{
	int res = 0;
	char *realpath = add_path(path);

	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);
	
	if (unlink(realpath) == -1) {
		res = -errno;
	} else {
		if (cynk.verbosity >= 3)
			verbose(TRUE, "UNLINK: %s", realpath);
		on_dead(path, ENTRY_IS_FILE); /* use path */
	}	
	g_free(realpath);
	return res;
}

static int cynk_rmdir(const char *path)
{
	int res = 0;
	char *realpath = add_path(path);
	
	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);
	
	if (rmdir(realpath) == -1) {
		res = -errno;
	} else {
		if (cynk.verbosity >= 3)
			verbose(TRUE, "RMDIR: %s", realpath);
		on_dead(path, ENTRY_IS_DIR); /* use path */
	}
	g_free(realpath);
	return res;
}

static int cynk_symlink(const char *from, const char *to)
{
	int res = 0;
	char *realfrom = add_path(from);
	char *realto = add_path(to);
	
	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);

	res = symlink(realfrom, realto);
	if (res == -1) {
		res = -errno;
	} else {
		if (cynk.verbosity >= 3)
			verbose(TRUE, "SYMLINK: %s", realto);
		on_born(to, ENTRY_IS_SYMLINK); /* use path */
	}

	g_free(realfrom);
	g_free(realto);
	return res;
}

static int cynk_rename(const char *from, const char *to)
{
	int res = 0;
	char *realfrom = add_path(from);
	char *realto = add_path(to);
	struct stat stbuf;
	uint32_t dir = ENTRY_IS_FILE;
	
	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);

	lstat(realfrom, &stbuf);
	if (S_ISDIR(stbuf.st_mode)) 
		dir = ENTRY_IS_DIR;

	if (rename(realfrom, realto) == -1) {
		res = -errno;
	} else {
		if (cynk.verbosity >= 3)
			verbose(TRUE, "MOVED_FROM: %s", realfrom);
		on_dead(from, dir); /* use from */
		
		if (cynk.verbosity >= 3)
			verbose(TRUE, "MOVED_TO: %s", realto);
		on_born(to, dir); /* use to */
	}
	g_free(realfrom);
	g_free(realto);
	return res;
}

static int cynk_link(const char *from, const char *to)
{
	int res = 0;
	char *realfrom = add_path(from);
	char *realto = add_path(to);

	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);
	
	res = link(realfrom, realto);
	if (res == -1) {
		res = -errno;
	} else {
		if (cynk.verbosity >= 3)
			verbose(TRUE, "LINK: %s", realto);
		on_born(to, ENTRY_IS_LINK); /* use path */
	}
	
	g_free(realfrom);
	g_free(realto);
	return 0;
}

static int cynk_chmod(const char *path, mode_t mode)
{
	int res;
	char *realpath = add_path(path);

	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);

	res = chmod(realpath, mode);

	g_free(realpath);
	
	if (res == -1)
		return -errno;

	return 0;
}

static int cynk_chown(const char *path, uid_t uid, gid_t gid)
{
	int res;
	char *realpath = add_path(path);

	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);
	
	res = lchown(realpath, uid, gid);

	g_free(realpath);

	if (res == -1)
		return -errno;

	return 0;
}

static int cynk_truncate(const char *path, off_t size)
{
	int res;
	char *realpath = add_path(path);

	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);
	
	res = truncate(realpath, size);
	
	g_free(realpath);
	if (res == -1)
		return -errno;

	return 0;
}

static int cynk_ftruncate(const char *path, off_t size,
			 struct fuse_file_info *fi)
{
	int res;
	(void) path;
	
	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);

	res = ftruncate(fi->fh, size);
	if (res == -1)
		return -errno;

	return 0;
}

static int cynk_utimens(const char *path, const struct timespec ts[2])
{
	int res;
	struct timeval tv[2];
	char *realpath;

	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);

	tv[0].tv_sec = ts[0].tv_sec;
	tv[0].tv_usec = ts[0].tv_nsec / 1000;
	tv[1].tv_sec = ts[1].tv_sec;
	tv[1].tv_usec = ts[1].tv_nsec / 1000;
	
	realpath = add_path(path);
	res = utimes(realpath, tv);
	g_free(realpath);
	if (res == -1)
		return -errno;

	return 0;
}

static int cynk_create(const char *path, mode_t mode, 
	struct fuse_file_info *fi)
{
	int fd, res = 0;
	char *realpath;
	
	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);

	realpath = add_path(path);
	fd = open(realpath, fi->flags, mode);
	if (fd == -1) {
		res = -errno;
	} else {
		if (cynk.verbosity >= 3)
			verbose(TRUE, "CREATE: %s", realpath);
		on_born(path, ENTRY_IS_FILE); /* use path */
	}
	fi->fh = fd;
	g_free(realpath);
	return res;
}

static int cynk_open(const char *path, struct fuse_file_info *fi)
{
	int fd;
	char *realpath;
	
	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);
	
	realpath = add_path(path);
	fd = open(realpath, fi->flags);
	if (fd == -1) {
		g_free(realpath);
		return -errno;
	}
	
	if (fi->flags & O_CREAT) {
		if (cynk.verbosity >= 3)
			verbose(TRUE, "CREAT_ON_OPEN: %s", realpath);
	}

	g_free(realpath);
	fi->fh = fd;
	return 0;
}

static int cynk_read(const char *path, char *buf, size_t size, off_t offset,
		    struct fuse_file_info *fi)
{
	int res;
	(void) path;
	
	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);
	
	res = pread(fi->fh, buf, size, offset);
	if (res == -1)
		res = -errno;

	return res;
}

static int cynk_write(const char *path, const char *buf, size_t size,
		     off_t offset, struct fuse_file_info *fi)
{
	int res;
	(void) path;
	
	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);
	
	res = pwrite(fi->fh, buf, size, offset);
	if (res == -1)
		res = -errno;

	return res;
}

static int cynk_statfs(const char *path, struct statvfs *stbuf)
{
	int res;
	char *realpath;
	
	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);
	
	realpath = add_path(path);
	res = statvfs(realpath, stbuf);
	g_free(realpath);
	if (res == -1)
		return -errno;

	return 0;
}

static int cynk_flush(const char *path, struct fuse_file_info *fi)
{
	int res;
	(void) path;
	
	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);
	
	/* This is called from every close on an open file, so call the
	   close on the underlying filesystem.	But since flush may be
	   called multiple times for an open file, this must not really
	   close the file.  This is important if used on a network
	   filesystem like NFS which flush the data/metadata on close() */
	res = close(dup(fi->fh));
	if (res == -1)
		return -errno;

	return 0;
}

static int cynk_release(const char *path, struct fuse_file_info *fi)
{
	(void) path;
	
	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);
	
	close(fi->fh);

	return 0;
}

static int cynk_fsync(const char *path, int isdatasync,
		     struct fuse_file_info *fi)
{
	int res;
	(void) path;
	
	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);

#ifndef HAVE_FDATASYNC
	(void) isdatasync;
#else
	if (isdatasync)
		res = fdatasync(fi->fh);
	else
#endif
		res = fsync(fi->fh);
	if (res == -1)
		return -errno;

	return 0;
}

#ifdef HAVE_SETXATTR
/* xattr operations are optional and can safely be left unimplemented */
static int cynk_setxattr(const char *path, const char *name, const char *value,
			size_t size, int flags)
{
	int res;
	char *realpath;
	
	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);

	realpath = add_path(path);
	res = lsetxattr(path, name, value, size, flags);
	g_free(realpath);
	if (res == -1)
		return -errno;
	return 0;
}

static int cynk_getxattr(const char *path, const char *name, char *value,
			size_t size)
{
	int res;
	char *realpath;
	
	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);
	
	realpath = add_path(realpath);
	res = lgetxattr(realpath, name, value, size);
	g_free(realpath);
	if (res == -1)
		return -errno;
	return res;
}

static int cynk_listxattr(const char *path, char *list, size_t size)
{
	int res;
	char *realpath;	
	
	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);
	
	realpath = add_path(realpath);
	res = llistxattr(realpath, list, size);
	g_free(realpath);
	if (res == -1)
		return -errno;
	return res;
}

static int cynk_removexattr(const char *path, const char *name)
{
	int res;
	char *realpath;
	
	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);
	
	realpath = add_path(realpath);
	res = lremovexattr(realpath, name);
	g_free(realpath);
	if (res == -1)
		return -errno;
	return 0;
}
#endif /* HAVE_SETXATTR */

static int cynk_lock(const char *path, struct fuse_file_info *fi, int cmd,
		    struct flock *lock)
{
	(void) path;
	
	clock_gettime(CLOCK_REALTIME, &cynk.sysc_stamp);

	return ulockmgr_op(fi->fh, cmd, lock, &fi->lock_owner,
			   sizeof(fi->lock_owner));
}

enum {
	CMD_SYNC = 1,
	CMD_CLOSE = 2,
};

static int send_cmd(void)
{
	int sockfd, len;
	struct sockaddr_un addr;
	char buf[MAX_BUF_LEN];

	if ((sockfd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
		perror2("failed to create socket");
		return -1;
	}
	
	addr.sun_family = AF_UNIX;
	strncpy(addr.sun_path, cynk.cmd_sockpath, sizeof(addr.sun_path));
	len = strlen(addr.sun_path) + sizeof(addr.sun_family);
	if (connect(sockfd, (struct sockaddr *) &addr, len) == -1) {
		perror2("failed to conenct to %s", cynk.cmd_sockpath);
		return -1;
	}
	
	snprintf(buf, MAX_BUF_LEN, "%d", cynk.cmd);
	if (send(sockfd, buf, strlen(buf), 0) == -1) {
		perror2("failed to send");
		return -1;
	}
	
	memset(buf, 0, MAX_BUF_LEN);
	if (recv(sockfd, buf, MAX_BUF_LEN, 0) == -1) {
		perror2("failed to receive");
		return -1;
	}
	fprintf(stdout, "%s\n", buf);
	fflush(stdout);
	return 0;
}

static void *process_cmd(void *data)
{
	int sockfd, cmd;
	socklen_t len;
	struct sockaddr_un addr;
	char buf[MAX_BUF_LEN];
	double elapsed;
	(void) data;

	while (1) {
		sockfd = accept(cynk.cmd_sock, (struct sockaddr *) &addr, &len);
		if (sockfd == -1) {
			perror2("failed to accept socket");
			break;
		}
		
		if (recv(sockfd, buf, MAX_BUF_LEN, 0) == -1) {
			perror2("failed to receive");
			break;
		}
		
		cmd = atoi(buf);
		switch(cmd) {
			case CMD_SYNC:
			if (cynk.verbosity >= 3)
				verbose(TRUE, "COMMAND: SYNC");
				elapsed = synchronize();
				memset(buf, 0, MAX_BUF_LEN);
				snprintf(buf, MAX_BUF_LEN, 
					"Sync with %s done, took %.3f second.",
					cynk.remote, elapsed);
				if (send(sockfd, buf, strlen(buf), 0) == -1)
					perror2("failed to send");
				close(sockfd);
				break;
			default:
				perror2("unknow command code %d");
				break;
		}
	}
	
	pthread_exit(NULL);
}

static int cmd_serv_init(void)
{
	int len, err;
	struct sockaddr_un addr;
	
	if ((cynk.cmd_sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
		perror2("failed to create socket");
		return -1;
	}
	
	addr.sun_family = AF_UNIX;
	strncpy(addr.sun_path, cynk.cmd_sockpath, sizeof(addr.sun_path));
	addr.sun_path[sizeof(addr.sun_path) - 1] = '\0';
	unlink(addr.sun_path);
	len = strlen(addr.sun_path) + sizeof(addr.sun_family);
	if (bind(cynk.cmd_sock, (struct sockaddr *)&addr, len) == -1) {
		perror2("failed to bind socket to %s", addr.sun_path);
		return -1;
	}

	if (listen(cynk.cmd_sock, 1) == -1) {
		perror2("failed to listen to socket %d", cynk.cmd_sock);
		return -1;
	}
	
	pthread_attr_setdetachstate(&cynk.cmd_thread_attr,
		PTHREAD_CREATE_JOINABLE);
	err = pthread_create(&cynk.cmd_thread, NULL, process_cmd, NULL);
	if (err) {
		error2(err, "create thread");
		return -EIO;
	}
	pthread_detach(cynk.cmd_thread);
	cynk.cmd_started = 1;

	return 0;
}

static int session_init(void)
{
	FILE *fp;
	char *file;

	/* write gid */
	file = g_strdup_printf("%s/pid", cynk.session);
	fp = fopen(file, "w+");
	if (fp == NULL) {
		perror2("failed to open file \"%s\"", file);
		return -1;
	}
	fprintf(fp, "%d", cynk.pid);
	fclose(fp);
	g_free(file);

	/* write paths */
	file = g_strdup_printf("%s/paths", cynk.session);
	fp = fopen(file, "w+");
	if (fp == NULL) {
		perror2("failed to open file \"%s\"", file);
		return -1;
	}
	fprintf(fp, "local=%s", cynk.mountpoint);
	fprintf(fp, "remote=%s", cynk.remote);
	fprintf(fp, "shadow=%s", cynk.local);
	fclose(fp);
	g_free(file);

	/* start control sock */
	cmd_serv_init();

	return 0;
}

static int cynk_command()
{
	struct stat st;

	char *hash = string_hash(cynk.local);
	cynk.session = g_strdup_printf("%s/cynk-%s/%s", SESSION_HOME,
		cynk.username, hash);
	cynk.cmd_sockpath = g_strdup_printf("%s/sock", cynk.session);
	
	if (stat(cynk.cmd_sockpath, &st) == -1) {
		if (errno == ENOENT)
			fprintf(stderr, "no cynk session associated with %s\n",
				cynk.local);
		exit(1);
	}
	if (!S_ISSOCK(st.st_mode)) {
		fprintf(stderr, "invalid socket %s\n", cynk.local);
		exit(1);
	}

	send_cmd();

	g_free(cynk.username);
	g_free(cynk.userhome);
	g_free(cynk.session);
	g_free(cynk.cmd_sockpath);
	g_free(cynk.local);
	
	return 0;
}

enum {
	MODE_AUTO,
	MODE_MANUAL,
};

#if FUSE_VERSION >= 26
static void *cynk_init(struct fuse_conn_info *conn)
#else
static void *cynk_init(void)
#endif
{
	int res;

#if FUSE_VERSION >= 26
	(void) conn;
#endif
	
	cynk.born = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
	if (!cynk.born)
		fatal(1, "failed to create hash table");
	
	cynk.dead = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
	if (!cynk.dead)
		fatal(1, "failed to create hash table");

	pthread_mutex_init(&cynk.lock_born, NULL);
	pthread_mutex_init(&cynk.lock_dead, NULL);

	cynk.repo_root = g_strdup_printf("%s/%s", cynk.local, REPO_HOME);
	cynk.trans_root = g_strdup_printf("%s/%s", cynk.local, TRANS_HOME);
	cynk.exclude_file = g_strdup_printf("%s/ex.filter", cynk.repo_root);
	cynk.include_file = g_strdup_printf("%s/ix.filter", cynk.repo_root);
	
	/* initial session and local repository */
	if (cynk.verbosity >= 2)
		verbose(FALSE, "* Initialing repository...");
	if (cynk.verbosity >= 3)
		verbose(FALSE, "> mkdir %s", cynk.session);
	if (g_mkdir_with_parents(cynk.session, S_IRWXU) == -1)
		fatal(1, "failed to create session directory %s", cynk.session);
	if (cynk.verbosity >= 3)
		verbose(FALSE, "> mkdir %s", cynk.local);
	if (cynk.verbosity >= 3)
		verbose(FALSE, "> mkdir %s", cynk.local);
	if (mkdir(cynk.local, S_IRWXU) == -1 && errno != EEXIST)
		fatal(1, "failed to create shadow repository %s", cynk.local);
	if (cynk.verbosity >= 3)
		verbose(FALSE, "> mkdir %s", cynk.repo_root);
	if (mkdir(cynk.repo_root, S_IRWXU) == -1 && errno != EEXIST)
		fatal(1, "failed to create directory %s", cynk.repo_root);
	if (cynk.verbosity >= 3)
		verbose(FALSE, "> mkdir %s", cynk.trans_root);
	if (mkdir(cynk.trans_root, S_IRWXU) == -1 && errno != EEXIST)
		fatal(1, "failed to create directory %s", cynk.trans_root);

	/* create repository root on remote side */
	cynk.rsync_common_opts = rsync_common_opts();
	res = run("rsync %s %s/ %s", cynk.repo_root, cynk.remote, 
		cynk.rsync_common_opts);

	if (cynk.verbosity >= 1)
		verbose(TRUE, "MIRROR: %s <==> %s", cynk.local, cynk.remote);
	/* two-way sync */
	res = run("rsync %s/ %s/ --exclude=/%s --temp-dir=%s %s",
		cynk.remote, cynk.local, REPO_HOME, TRANS_HOME, 
			cynk.rsync_common_opts);

	res = run("rsync %s/ %s/ --exclude=/%s --temp-dir=%s %s",
		cynk.local, cynk.remote, REPO_HOME, TRANS_HOME, 
			cynk.rsync_common_opts);

	pthread_attr_init(&cynk.cmd_thread_attr);

	session_init();

	if (cynk.mode != MODE_MANUAL)
		start_sync_thread();

	return NULL;
}

static void cynk_destroy(void *data_)
{
	int res;
	(void) data_;
	
	if (cynk.verbosity >= 1)
		verbose(TRUE, "STOP");
	
	if (cynk.mode != MODE_MANUAL)
		stop_sync_thread();
	
	if (cynk.verbosity >= 2)
		verbose(FALSE, "* Stop control server thread...");
	close(cynk.cmd_sock);
	pthread_join(cynk.cmd_thread, NULL);
	unlink(cynk.cmd_sockpath);
	
	if (cynk.verbosity >= 3)
		verbose(FALSE, "cleanup...");
	/* cleanup remote repository */
	res = run("rm -rf %s", cynk.repo_root);
	res = run("rsync %s/ %s/ --delete --filter='+ /%s' --filter='- /*' %s",
		cynk.local, cynk.remote, REPO_HOME, cynk.rsync_common_opts);
	
	/* move repository back, safe since FUSE has unmounted */
	if (cynk.verbosity >= 2)
		verbose(FALSE, "* Moving repository back...");
	if (cynk.verbosity >= 3)
		verbose(FALSE, "> rm %s", cynk.mountpoint);
	if (rmdir(cynk.mountpoint) == -1)
		perror2("failed to remove %s", cynk.mountpoint);
	if (cynk.verbosity >= 3)
		verbose(FALSE, "> mv %s %s", cynk.local, cynk.mountpoint);
	if (rename(cynk.local, cynk.mountpoint) == -1)
		perror2("failed to move %s to %s", cynk.local, cynk.mountpoint);
	
	res = run("rm -rf %s", cynk.session);
	
	pthread_mutex_destroy(&cynk.sync_lock);
	pthread_attr_destroy(&cynk.cmd_thread_attr);
	
	pthread_mutex_destroy(&cynk.lock_born);
	pthread_mutex_destroy(&cynk.lock_dead);
	g_hash_table_destroy(cynk.born);
	g_hash_table_destroy(cynk.dead);
	
	g_free(cynk.cmd_sockpath);
	g_free(cynk.session);
	g_free(cynk.userhome);
	g_free(cynk.username);
	g_free(cynk.rsync_common_opts);
	g_free(cynk.exclude_file);
	g_free(cynk.include_file);
	g_free(cynk.trans_root);
	g_free(cynk.repo_root);
	g_free(cynk.local);
	g_free(cynk.remote);
	g_free(cynk.mountpoint);
	
	if (cynk.verbosity >= 3)
		verbose(FALSE, "bye.");
}

static struct fuse_operations cynk_oper = {
	.init		= cynk_init,
	.destroy	= cynk_destroy,
	.getattr	= cynk_getattr,
	.fgetattr	= cynk_fgetattr,
	.access		= cynk_access,
	.readlink	= cynk_readlink,
	.opendir	= cynk_opendir,
	.readdir	= cynk_readdir,
	.releasedir	= cynk_releasedir,
	.mknod		= cynk_mknod,
	.mkdir		= cynk_mkdir,
	.symlink	= cynk_symlink,
	.unlink		= cynk_unlink,
	.rmdir		= cynk_rmdir,
	.rename		= cynk_rename,
	.link		= cynk_link,
	.chmod		= cynk_chmod,
	.chown		= cynk_chown,
	.truncate	= cynk_truncate,
	.ftruncate	= cynk_ftruncate,
	.utimens	= cynk_utimens,
	.create		= cynk_create,
	.open		= cynk_open,
	.read		= cynk_read,
	.write		= cynk_write,
	.statfs		= cynk_statfs,
	.flush		= cynk_flush,
	.release	= cynk_release,
	.fsync		= cynk_fsync,
#ifdef HAVE_SETXATTR
	.setxattr	= cynk_setxattr,
	.getxattr	= cynk_getxattr,
	.listxattr	= cynk_listxattr,
	.removexattr	= cynk_removexattr,
#endif
	.lock		= cynk_lock,

	.flag_nullpath_ok = 1,
};

static int cynk_fuse_main(struct fuse_args *args)
{
#if FUSE_VERSION >= 26
	return fuse_main(args->argc, args->argv, &cynk_oper, NULL);
#else
	return fuse_main(args->argc, args->argv, &cynk_oper);
#endif
}

static void usage(const char *progname)
{
	printf(
"usage: %s repository [[user@]host:]dir [options]\n"
"\n"
"cynk options:\n"
"    -h   --help            print help\n"
"    -V   --version         print version\n"
"    -S PATH                force a sync\n"
"    -m                     manual sync mode\n"
"    -t N                   duration for next check (60s)\n"
"    -b N                   sync only if filesystem is stable for (30s)\n"
"    -d                     debug cynk only\n"
"    -D                     debug cynk and FUSE\n"
"    -n   --dry-run         dry run\n"
"\n", progname);
}

static char * chk_norm_dir(const char *path)
{
	struct stat st;
	char *cp, *s;
	
	/* check if a valid path */
	if (stat(path, &st)) {
		error3("invalid directory %s", path);
		return NULL;
	}
	if (!S_ISDIR(st.st_mode)) {
		error3("%s is not a directory", path);
		return NULL;
	}
	
	s = realpath(path, NULL);
	cp = g_strdup(s);
	free(s);
	return cp;
}

static int cynk_opt_proc(void *data, const char *arg, int key,
                          struct fuse_args *outargs)
{
	(void) data;

	switch (key) {
	case FUSE_OPT_KEY_OPT:
		return 1;

	case FUSE_OPT_KEY_NONOPT:
		if (!cynk.local) {
			cynk.local = chk_norm_dir(arg);
			if (!cynk.local)
				exit(1);
		} else if (!cynk.remote) {
			if (strchr(arg, ':')) { /* host */
				cynk.remote = g_strdup(arg);
			} else {
				cynk.remote = chk_norm_dir(arg);
				if (!cynk.remote)
					exit(1);
			}
		}
		return 0;

	case KEY_HELP:
		usage(outargs->argv[0]);
		fuse_opt_add_arg(outargs, "-ho");
		cynk_fuse_main(outargs);
		exit(0);

	case KEY_VERSION:
		printf("CloudSync-FUSE version %s\n", PACKAGE_VERSION);
#if FUSE_VERSION >= 25
		fuse_opt_add_arg(outargs, "--version");
		cynk_fuse_main(outargs);
#endif
		exit(0);

	case KEY_FOREGROUND:
		cynk.foreground = 1;
		return 1;

	case KEY_DEBUG:
		cynk.debug = 1;
		return 0;
	
	case KEY_DEBUG_ALL:
		cynk.debug = 2;
		return 0;
	
	case KEY_DRYRUN:
		cynk.dryrun = 1;
		return 0;

	case KEY_CMD_SYNC:
		cynk.cmd = CMD_SYNC;
		cynk.local = chk_norm_dir(arg + 2);
		return 0;

	case KEY_MODE_MANUAL:
		cynk.mode = MODE_MANUAL;
		return 0;

	case KEY_DURATION:
		cynk.duration = atoi(arg + 2);
		return 0;

	case KEY_STABLE:
		cynk.stable = atoi(arg + 2);
		return 0;

	default:
		error3("internal error");
		abort();
	}
}

#if FUSE_VERSION == 25
static int fuse_opt_insert_arg(struct fuse_args *args, int pos,
                               const char *arg)
{
	assert(pos <= args->argc);
	if (fuse_opt_add_arg(args, arg) == -1)
		return -1;

	if (pos != args->argc - 1) {
		char *newarg = args->argv[args->argc - 1];
		memmove(&args->argv[pos + 1], &args->argv[pos],
			sizeof(char *) * (args->argc - pos - 1));
		args->argv[pos] = newarg;
	}
	return 0;
}
#endif

int main(int argc, char *argv[])
{
	int res;
	struct passwd *pwd;
	struct fuse_args args = FUSE_ARGS_INIT(argc, argv);

	memset(&cynk, 0, sizeof(cynk));
	
	cynk.progname = argv[0];
	cynk.duration = 60;
	cynk.stable = 30;
	cynk.mode = MODE_AUTO;
	cynk.uid = getuid();
	cynk.pid = getpid();
	
	pwd = getpwuid(cynk.uid);
	if (!pwd)
		fatal(1, "failed to get pwd for uid %d\n", cynk.uid);
	cynk.gid = pwd->pw_gid;
	cynk.username = g_strdup(pwd->pw_name);
	cynk.userhome = g_strdup(pwd->pw_dir);
	
	if (fuse_opt_parse(&args, &cynk, cynk_opts, cynk_opt_proc) == -1)
		exit(1);
	
	if (cynk.debug >= 1) {
		cynk.foreground = 1;
		fuse_opt_insert_arg(&args, 1, "-f");
		if (!cynk.verbosity)
			cynk.verbosity = 3;
		if (cynk.debug >= 2)
			fuse_opt_insert_arg(&args, 1, "-d");
	}

	if (cynk.cmd) {
		fuse_opt_free_args(&args);
		return cynk_command();
	}
	
	if (!cynk.local || !cynk.remote) {
		fprintf(stderr, 
			"%s: missing repository\n"
			"see `%s -h' for usage\n", 
			cynk.progname, cynk.progname);
		fuse_opt_free_args(&args);
		exit(1);
	}

	/* create a fake mountpoint to shadow local repository */
	cynk.mountpoint = cynk.local;
	char *dirname = g_path_get_dirname(cynk.mountpoint);
	char *basename = g_path_get_basename(cynk.mountpoint);
	char *tmpstr = rand_str(TEMP_STR_LEN);

	cynk.local = g_strdup_printf("%s/.cynk-%s-%s", dirname, basename, tmpstr);
	cynk.session = g_strdup_printf("%s/cynk-%s/%s", SESSION_HOME,
		cynk.username, tmpstr);
	cynk.cmd_sockpath = g_strdup_printf("%s/sock", cynk.session);
	
	if (cynk.verbosity >= 1)
		verbose(TRUE, "INIT");
	
	if (cynk.verbosity >= 2)
		verbose(FALSE, "* Shadowing repository...");
	if (cynk.verbosity >= 3)
		verbose(FALSE, "> mv %s %s", cynk.mountpoint, cynk.local);
	if (rename(cynk.mountpoint, cynk.local) == -1)
		fatal(1, "failed to move %s to %s", cynk.mountpoint, cynk.local);
	if (cynk.verbosity >= 3)
		verbose(FALSE, "> mkdir %s", cynk.mountpoint);
	if (mkdir(cynk.mountpoint, S_IRWXU) == -1 && errno != EEXIST)
		fatal(1, "failed to create shadow repository %s", cynk.mountpoint);

	g_free(dirname);
	g_free(basename);
	g_free(tmpstr);

	/* insert mountpoint back */
	fuse_opt_insert_arg(&args, 1, cynk.mountpoint);

	umask(0);
	res = cynk_fuse_main(&args);

	fuse_opt_free_args(&args);
	
	return res;
}

/* EOF */
